/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.tle;

import ore.PositionVelocity;
import ore.errors.OrekitException;
import ore.time.AbsoluteDate;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.SortedSet;
import java.util.zip.GZIPInputStream;

/** 
 * This class reads and handles a series of TLEs for one satellite
 * from a stream.
 * 
 * This class provides bounded ephemerides by finding the best initial
 * TLE to propagate and then handling the propagation.  It is not
 * multi- thread safe.
 *
 * @author Fabien Maussion
 */
public class TLESeries 
    extends ore.time.ChronologicalSet<TLE>
{
    /** 
     * The satellite id.
     */
    public final int satelliteNumber;
    /** 
     * International designator.
     */
    public final String internationalDesignator;

    /** 
     * Previous TLE in the cached selection.
     */
    private TLE previous;
    /** 
     * Next TLE in the cached selection.
     */
    private TLE next;
    /** 
     * Last used TLE.
     */
    private TLE lastTLE;
    /** 
     * Associated propagator.
     */
    private TLEPropagator lastPropagator;
    /** 
     * Date of the first TLE.
     */
    private AbsoluteDate firstDate;
    /** 
     * Date of the last TLE.
     */
    private AbsoluteDate lastDate;


    /** 
     * Read TLE entries from input.
     * 
     * @param in Input to read (may be gzip compressed).  The stream
     * must be closed by the caller.
     * 
     * @exception IOException Error from input
     * @exception OrekitException File format error
     */
    public TLESeries(InputStream in)
        throws IOException, OrekitException
    {
        super ();

        final BufferedReader r = new BufferedReader(new InputStreamReader(InputCompressed(in),"US-ASCII"));

        int satid = 0;
        String satdes = null;
        {        
            for (String line1 = r.readLine(); line1 != null; line1 = r.readLine()) {
                final String line2 = r.readLine();
                if (line2 == null) {
                    throw new OrekitException("Missing second line in TLE");
                }
                else if (!TLE.IsFormatOK(line1, line2)) {
                    throw new OrekitException("Non-TLE line in TLE data file");
                }
                else {
                    final int satNum = Integer.parseInt(line1.substring(2, 7).replace(' ', '0'));
                    final String iD = line1.substring(9, 17);
                    if (0 == satid && null == satdes) {
                        satid = satNum;
                        satdes = iD;
                    }
                    else if (satNum == satid)
                        this.add(new TLE(line1, line2));
                }
            }
        }
        this.satelliteNumber = satid;
        this.internationalDesignator = satdes;
    }
    /**
     * Available to a specific kind of subclass.
     * 
     * @param satid The satellite id.
     * @param satdes International designator.
     */
    protected TLESeries(int satid, String satdes){
        super ();
        if (0 < satid && null != satdes){
            this.satelliteNumber = satid;
            this.internationalDesignator = satdes;
        }
        else
            throw new IllegalArgumentException();
    }


    /** 
     * @return The first date in this series.
     */
    public final AbsoluteDate getFirstDate() {
        AbsoluteDate first = this.firstDate;
        if (null == first)
            return (this.firstDate = this.first().getDate());
        else
            return first;
    }
    /** 
     * @return The last date in this series.
     */
    public final AbsoluteDate getLastDate() {
        AbsoluteDate last = this.lastDate;
        if (null == last)
            return (this.lastDate = this.last().getDate());
        else
            return last;
    }
    /** 
     * Compute position and velocity for a date from the single
     * nearest TLE.  The available precision depends on the range of
     * TLEs in this series, from {@link #getFirstDate() first date} to
     * {@link #getLastDate() last date}.
     * 
     * @param date Final date
     * 
     * @return Final position and velocity
     * 
     * @exception OrekitException The underlying propagator cannot be
     * initialized
     */
    public final PositionVelocity getPositionVelocity(AbsoluteDate date)
        throws OrekitException
    {
        final TLE toExtrapolate = this.getClosestTLE(date);
        if ((this.lastTLE == null) || (toExtrapolate.getDate().compareTo(this.lastTLE.getDate()) != 0)) {
            this.lastTLE = toExtrapolate;
            this.lastPropagator = TLEPropagator.Select(lastTLE);
        }
        return this.lastPropagator.getPositionVelocity(date);
    }
    /** 
     * Get the TLE closest to the selected date.
     * @param date For date
     * @return Best TLE to propagate.
     */
    public final TLE getClosestTLE(AbsoluteDate date) {

        if ((this.previous != null) && (date.durationFrom(this.previous.getDate()) >= 0) &&
            (this.next     != null) && (date.durationFrom(this.next.getDate())     <= 0))
        {
            if (this.next.getDate().durationFrom(date) > 
                date.durationFrom(this.previous.getDate()))

                return this.previous;
            else
                return this.next;
        }
        /*
         * Reset the selection 
         */
        this.previous = null;
        this.next     = null;
        /*
         * Select head and tail sets
         */
        Brackets<TLE> brackets = this.brackets(date);

        if (null == brackets.previous)
            return brackets.next;

        else if (null == brackets.next)
            return brackets.previous;

        else {
            this.previous = brackets.previous;
            this.next = brackets.next;

            if (this.next.getDate().durationFrom(date) > 
                date.durationFrom(this.previous.getDate()))

                return this.previous;
            else 
                return this.next;
        }
    }

    private final static int BUFGZTest = (1024*1024);
    /** 
     * Checks if a file is compressed or not.
     * 
     * @param in Input to check for GZIP compression
     * 
     * @return Buffered input 
     * 
     * @exception IOException Mark/reset not supported 
     */
    private static BufferedInputStream InputCompressed(final InputStream in)
        throws IOException
    {
        BufferedInputStream filter = new BufferedInputStream(in);
        filter.mark(BUFGZTest);
        try {
            if (new GZIPInputStream(filter).read() != -1){
                filter = new BufferedInputStream(new GZIPInputStream(filter));
                filter.mark(BUFGZTest);
            }
        }
        catch (IOException e) {
        }
        filter.reset();
        return filter;
    }
}
